package com.atguigu.tingshu.user.service.impl;

import cn.binarywang.wx.miniapp.api.WxMaService;
import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.IdUtil;
import com.atguigu.tingshu.common.constant.RedisConstant;
import com.atguigu.tingshu.common.execption.GuiguException;
import com.atguigu.tingshu.common.result.ResultCodeEnum;
import com.atguigu.tingshu.common.util.AuthContextHolder;
import com.atguigu.tingshu.model.user.UserInfo;
import com.atguigu.tingshu.user.mapper.UserInfoMapper;
import com.atguigu.tingshu.user.service.UserInfoService;
import com.atguigu.tingshu.vo.user.UserInfoVo;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import io.micrometer.common.util.StringUtils;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.error.WxErrorException;
import org.apache.http.impl.client.HttpClients;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

@Slf4j
@Service
@SuppressWarnings({"all"})
public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo> implements UserInfoService {

	@Autowired
	private UserInfoMapper userInfoMapper;

    @Autowired
    private WxMaService wxMaService;

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * @Description 微信授权登录
     * 1.结合依赖,调用微信授权接口,获取openId
     * 2.判断该用户是否已注册(使用openId查表)
     * 3.如果用户没有注册,那么就要实现注册并登录(在注册的时候,需要初始化账户余额信息)
     * 4.如果用户已经注册,那么生成token,保存token + 用户信息到redis
     *   并且返回token给前端.
     */
    @Override
    public Map<String, String> wxLogin(String code) {
        try {
            WxMaJscode2SessionResult sessionInfo = wxMaService.getUserService().getSessionInfo(code);
            String openid = sessionInfo.getOpenid();
            log.info("UserInfoServiceImpl wxLogin openId {}", openid);
            if (StringUtils.isEmpty(openid)) {
                throw new GuiguException(ResultCodeEnum.LOGIN_AUTH_FAIL);
            }

            UserInfo userInfo = userInfoMapper.selectOne(new LambdaQueryWrapper<UserInfo>()
                    .eq(UserInfo::getWxOpenId, openid));
            if (Objects.isNull(userInfo)) {
                // 实现注册并登录
                userInfo = new UserInfo();
                userInfo.setWxOpenId(openid);
                userInfo.setNickname("听友" + IdUtil.fastSimpleUUID().substring(0,10));
                userInfo.setAvatarUrl("https://oss.aliyuncs.com/aliyun_id_photo_bucket/default_handsome.jpg");
                userInfo.setIsVip(0);
                userInfo.setStatus("0");
                userInfoMapper.insert(userInfo);

                // TODO 首次注册用户,需要完成账户余额初始化工作
                // 后续使用kafka实现
            }

            UserInfoVo userInfoVo = BeanUtil.copyProperties(userInfo, UserInfoVo.class);
            String token = IdUtil.fastSimpleUUID();
            redisTemplate.opsForValue().set(
                    RedisConstant.USER_LOGIN_KEY_PREFIX + token,
                    userInfoVo,
                    RedisConstant.USER_LOGIN_KEY_TIMEOUT,
                    TimeUnit.SECONDS);
            return Map.of("token", token);
        } catch (WxErrorException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * @Description 修改用户信息的时候,需要保证MySQL和redis的数据一致性
     * 因为查询用户登录信息是从redis中查询,但是目前用户已经修改了数据库中的数据,所以需要同步数据库和redis中的数据一致
     *
     * 使用方案:
     * 在处理MySQL和Redis之间的数据一致性问题时，通常采用的策略包括同步删除、延时双删、异步监听和多重保障方案。
     * 这些策略旨在确保数据库中的数据变更能够及时且准确地反映在缓存中，从而避免数据不一致导致的业务逻辑错误或性能下降。
     *
     * 延时双删策略是保证更高数据一致性的方案之一。
     * 这种策略的具体步骤如下：先删除缓存，然后更新数据库，经过一段时间延迟后再次删除缓存。
     * 通过这样的方式，可以较大程度地减少由于并发操作导致的数据不一致问题。
     *
     * 具体实现过程通常是这样的：当有数据更新操作时，首先删除缓存中的数据（第一次删除），
     * 然后执行数据库更新操作，在数据库事务提交后，
     * 通过延时（比如500毫秒）再次尝试删除缓存中的数据（第二次删除）。
     * 这样做的目的是等待潜在的并发读操作完成，确保这些读操作不会因为缓存失效而从数据库加载旧数据并重新写入缓存，造成脏数据问题。
     * 延时的时间通常根据具体的业务逻辑耗时加上一定的缓冲时间来确定。
     *
     * 延时双删虽能提高数据一致性，但并不是无懈可击。
     * 如果延时时间内仍有旧数据的读取和缓存写入操作，依然可能导致数据不一致性问题。
     * 此外，延时的设定也需谨慎，过长或过短都可能影响系统的整体性能和数据准确性。
     * 因此，延时双删更多适用于读多写少、对数据一致性要求较高的场景。
     *
     * 总的来说，MySQL和Redis之间的数据一致性问题可以通过多种策略来应对，
     * 其中延时双删策略在高一致性需求的场景下特别有用，但需要合理设置延时以确保效果。
     * 另外，结合其他策略和技术手段，可以进一步提高系统的健壮性和数据的准确性。
     * 在实际应用中，应根据具体业务需求和技术条件灵活选择，以达到最优的系统性能和数据一致性。
     */
    @Override
    public void updateUser(String token, UserInfoVo userInfoVo) {
        String redisKey = RedisConstant.USER_LOGIN_KEY_PREFIX + token;
        log.info("updateUser getRedisKey is {}", redisKey);
        // 先删除redis
        redisTemplate.delete(redisKey);

        // 更新数据库
        UserInfo userInfo = BeanUtil.copyProperties(userInfoVo, UserInfo.class);
        userInfo.setId(AuthContextHolder.getUserId());
        userInfoMapper.updateById(userInfo);

        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        // 第二次删除redis
        redisTemplate.delete(redisKey);

        // 为了后续可以继续从redis中查询到用户信息,需要重新将更新后的用户信息保存到redis中
        UserInfo userInfoByUpdate = userInfoMapper.selectById(userInfo.getId());
        userInfoVo = BeanUtil.copyProperties(userInfoByUpdate, UserInfoVo.class);
        // 更新用户信息之后,重置TTL
        redisTemplate.opsForValue().set(redisKey, userInfoVo, RedisConstant.USER_LOGIN_KEY_TIMEOUT, TimeUnit.SECONDS);
    }
}
